MIDI Tool Set
Volume Number: 5
Issue Number: 11
Column Tag: MIDI Mac
Apple's MIDI Manager
By Don Veca, San Jose, CA
Introduction
The MIDI Manager is Apple’s new standard MIDI tool set. It enables developers to
cleanly transfer MIDI data to and from synthesizers and other con currently running
applications. It supplies a complete MIDI tool set which is as powerful as any existing
implementation, yet strictly adheres to system guidelines. Included with the MIDI
Manager are the Apple MIDI Driver and PatchBay.
The Apple MIDI Driver interprets all incoming and out going MIDI messages and
handles all serial port I/O. This includes various modes of time code generation and
concurrent asynchronous serial port communication. PatchBay is Apple’s graphical
user interface for the MIDI Manager. Supplied as a utility application and a Desk
Accessory, PatchBay enables users to intuitively route MIDI data and time code to and
from the Apple MIDI Driver as well as throughout multiple applications running under
Multi finder. The MIDI Manager proper, however, supplies all internal
communications and supplies a thorough and very powerful MIDI tool set.
Main Concepts
From the music software developer’s point of view, the main components of the
MIDI Manager are clients and ports. A client is any program that uses the MIDI
Manager’s facilities. Although an application usually needs only one client, it can
actually have several. Each client can, and usually does, create multiple ports which
are basically unidirectional streams of MIDI information. Each port can be either a
time port, which provides the MIDI Manager’s timing facilities, or a data port, which
is used to read or write MIDI packets. The data ports are further divided into input and
output ports. A client’s ports can be connected to one of its own ports or to any other
client’s ports; however, input ports can only be connected to output ports (and vice
versa). Time ports enable port synchronization: when a time port’s clock ticks, all
clocks of all ports connected to it tick synchronously. Data ports send and receive MIDI
data such as note-on/note-off or system exclusive messages. Input and output ports
respectively receive and send MIDI data to the ports of MIDI drivers or other MIDI
Manager compatible applications running con currently under Multi finder. We will
now illustrate the proper use of the MIDI Manager through the explanation of a sample
application called MIDIArp.
The MIDIArp Demo Application
MIDIArp is a simple arpeggiator program [tone generator for those who are not
blessed with a huge vocabulary like Don’s. -ed] that illustrates the use of the MIDI
Manager’s data and timing facilities. MIDIArp simply reads in note-on data and
arpeggiates it until corresponding note-off data is received. The basic flow of MIDIArp
is quite simple: after MIDIArp initializes the various Macintosh Managers, it signs
into the MIDI Manager, calls a routine to set up its time, input, and output ports, pops
up its main dialog box, and cycles through its main event loop. The main event loop
simply checks for user (console) input, and adjusts its arpeggio direction and speed
parameters accordingly. Once the quit button is selected, MIDIArp signs out from the
MIDI Manager and terminates. During the main event loop, however, MIDIArp
continually arpeggiates its MIDI input at interrupt level via its readHook and timeProc
routines.
MIDIArp.h
MIDIArp includes a header file, MIDIArp.h, which contains its main data
structures and symbolic constants. In addition to the standard user interface constants
for menu and dialog resource ID’s, constants are defined to identify our own resource
of type ‘port’. We define three such ‘port’ resources: one for each MIDI Manager port
we intend to use. This is necessary in order to save the state of our patch between each
launch of the application. For readability, several MIDIArp constants are defined
followed by several constants local to the MIDI Manager itself. Specifically, we define
our client and ‘ICN#’ resource ID, both of which are used to sign in to the MIDI
Manager. The client ID is a four-byte OSType (and by convention, although not by
necessity, our client ID is used as our application signature). We then define our port
ID’s which are also of type OSType. The actual ports will be displayed by PatchBay
from top to bottom in ascending alphabetical order. (It is recommended that time ports
be displayed above the data ports that are synchronized to them, and that an
application’s input and output ports be displayed in the reverse order of that used by
the Apple MIDI Driver.)
After several MIDI Manager parameters are symbolically defined, we define our
main data structures. The first data structure that we will need is a NoteInfo record
which contains fields to store the MIDI channel, key number, and key velocity of each
incoming note-on message. The NoteInfo record is itself an individual field of the main
MIDIArp data structure ArpParams. The ArpParams structure contains various fields
of information concerning the current state of the MIDIArp client. In particular, it
contains a field called Locked, which is used to prevent the structure from being
modified while it is in use. Incoming MIDI data (notes that MIDIArp is currently
arpeggiating) are stored in an array of NoteInfo records called NoteTbl. Other fields
hold information about things such as tempo and current arpeggiation pattern (e.g.,
whether we are currently going up or down, etc.). NextNoteOn is a field used to keep
track of the exact time the next note is to be played. Finally, the ArpParams structure
allows the storage of each port’s reference number.
MIDIArp.c
The main source file for MIDIArp is MIDIArp.c. Here we first define several
global variables, including an ArpParams record called ArpGlobals, a variable to hold
our current arpeggiation speed ID, and a flag (GManualPatch) to indicate whether the
current port configuration (patch) has been set up by a (previously saved) PatchBay
patch or needs to be reconfigured by MIDIArp itself (based on its last configuration).
The main() routine of MIDIArp calls InitThings() to initialize the Macintosh
Managers, ArpInit() which signs-in to the MIDI Manager and sets up our ports,
StartDialog() to bring up the main dialog box with default settings, and then
RunDialog() to handle events. When the Quit button is finally hit, ArpClose() is called
to sign-out from the MIDI Manager, and the main dialog box is shut down through a
call to StopDialog(). InitThings(), in addition to initializing the various managers of
the Macintosh, sets up a standard menu bar and seeds the random number generator for
random arpeggiation. ArpInit(), on the other hand, completely sets up MIDIArp’s
MIDI Manager environment. In order to use the MIDI Manager we must first make
sure that it is currently installed. This is achieved through a call to SndDispVersion()
(sound dispatch version). Given the constant midiToolNum, SndDispVersion() returns
the version of the currently installed MIDI Manager or zero if the MIDI Manager is not
installed. Once we have concluded that the MIDI Manager is installed, we must sign in
to the MIDI Manager (before we make any other calls) by calling MIDISignIn(). We
pass as arguments to MIDISignIn() our client ID, our client reference constant, a
handle to our ‘ICN#’ resource, and our client name string. The client ID is used for
future MIDI Manager calls and allows other clients, such as patchers, to get
information about us. The client reference constant, or refCon, is a general purpose
parameter that is only really needed by certain types of applications (such as device
drivers). The handle to the ‘ICN#’ resource and the client name string are passed to
allow PatchBay (or any other clients) to display them via MIDIGetClientIcon() and
MIDIGetClientName(), respectively.
The next thing we do in ArpInit() is add our time, input, and output ports via
MIDIAddPort() and connect them accordingly. The tricky part is determining whether
the ports will automatically be connected via a saved PatchBay patch, or whether we
must connect them ourselves. Therefore, we set the global flag GManualPatch to true
before we add any ports allowing us to determine by the return value of MIDIAddPort()
whether or not we need to connect the ports ourselves. If MIDIAddPort() returns
midiVConnectMade (virtual connection resolved), then we know that the ports were
virtually connected by someone else via MIDIConnectTime() or MIDIConnectData().
In this case we simply set GManualPatch to false indicating that we do not have to
manually patch together the ports. If GManualPatch is still true after all ports have
been added, then we will call PatchPorts() to do our own patching.
The first port we add in ArpInit() is our time port or time base. We do this by
calling MIDIAddPort(). This call will create a new port with attributes as described in
an InitParams data structure; therefore, we must first set one up. The init record
contains various information including items such as the port ID, the port type (time,
input, output, invisible time) the port’s readHook, the time format, port name, etc.,
and a port reference constant (refCon). If the port’s readHook routine is to be called at
interrupt level, then the refCon can be used to store the contents of the application’s
A5 register for global variable access. The same basic strategy is used for creating the
input and output ports; however, MIDIArp by default wants its data ports to be sync’d
to its time port. So before we actually add the port, we set the timeBase field of the
InitParams record to our time port’s reference number (previously obtained when we
added our time port).
Now that all the ports have been added, we check GManualPatch, and, if it’s still
true, we call PatchPorts() to patch the ports as they were when we last quit. To
reconfigure our port connections, we must read in the ‘port’ resource of each port.
The port resource is nothing more than the saved result of MIDIGetPortInfo() from
our last session. MIDIGetPortInfo() returns a handle to a record containing the port
type, time base of the port, and a list of all its connections.
To reconfigure our time port, we first check its port info record (of its ‘port’
resource) to see if we should be sync’d to another client’s time base. If so, we call
MIDIConnectTime(), connecting the external time port to our time port (slaving us to
it). If the result of MIDIConnectTime() is midiVConnectErr, then the external port’s
owner is not currently signed in; otherwise, we must set our sync mode to
externalSync. Next, we check to see if we are supposed to be the time base of one or
more external ports. If so, we connect our time port to each.
It’s a little less complicated to reconfigure our input and output ports. All we
have to do is connect our input and output port to each of the ports listed in the
PortInfo record contained in the corresponding ‘port’ resource. The last thing we need
to do in ArpInit() is start our time port’s clock by calling MIDIStartTime(). Now that
everything is set up, we simply cycle through our main event loop, waiting for the
MIDI Manager to call our readHook routine with incoming MIDI data.
In addition to handling user events, the main event loop periodically checks to see
if an external time base has suddenly been connected to us. This is achieved by first
detecting that “something in MIDI Manager world has changed,” and then by checking
our time port’s info record to see if the MIDI Manager reports that we currently have
an external time base. This situation arises if another client attempts to connect
themselves to us or if a user of a patcher program (such as Patchbay) manually
connects an external time port to ours. We check for either event in our main event
loop by calling MIDIWorldChanged(). If MIDIWorldChanged() returns true, then we
know that something in the current world has changed; however, this could be caused
by a client signing in or out, a port being added or removed, or a connection being made
or removed. Therefore, we must call MIDIGetPortInfo() on our time port and check
whether the returned port information record reports that we currently have an
external time base. If so, we then set our time port to external sync via
MIDISetSync(); otherwise, we set it back to midiInternalSync (in the case that we
were currently in external sync mode). The only thing left now is to get and process
incoming MIDI data; this operation is handled at interrupt level through use of a
readHook and a timeProc routine.
Before jumping into the details, a general explanation of the interrupt level
control flow of MIDIArp’s readHook and timeProc is needed. The readHook is called by
the MIDI Manager whenever an incoming message becomes “ current.” When the first
note of an input sequence is sent to MIDIArp’s input port, the MIDI Manager calls
MIDIArp’s readHook which then copies it into the application’s note table buffer and
calls the timeProc to take care of the output. When called, the timeProc determines
the next note in the note table to be played, writes out the selected note, and then tells
the MIDI Manager exactly when to call the timeProc for the next output. In other
words, the only time the MIDIArp application calls its timeProc is from within its
readHook, and this is done only when it receives the first note of an input sequence.
After that, the timeProc itself is responsible for setting up its next wake-up.
As summarized above, MIDIArp’s readHook ArpReader() reads all incoming data,
and starts a series of timeProc wake-ups which create the arpeggiation effect. As you
may recall, the refCon field of the initialization record of each port was set to point to
our application’s global variables and passed as an argument to MIDIAddPort() when
the port was originally created; additionally, the readHook field of the input port’s
initialization record was set to the address of ArpReader(). When the MIDI Manager
calls a port’s readHook, it passes two parameters: the next “ current” MIDIPacket,
and the port’s refCon value. Because the MIDI Manager calls ArpReader() at interrupt
level, the readHook first sets up our A5 world. This is achieved by calling the System
routine SetA5() with the refCon parameter. Since we will be modifying the note table
in the ArpGlobals record, we must check whether any other routine is currently
reading it by checking the Locked field. If it is locked, we simply return
midiKeepPacket, which tells the MIDI Manager to save the new packet -- we’ll get it
later. The packet is of interest to MIDIArp only if it is does not contain a MIDI Manager
system specific message (indicated in the message type field of the flags byte), and does
contain a note-on message. If these conditions are satisfied, then we set the return
value to midiMorePacket, telling the MIDI Manager that we now have this packet and
that we want the next one.
Before we actually return, however, we process the current packet. If the status
byte in the packet indicates a note-on message, then we copy it into our note table and
increment our current note count. If our note count is now equal to 1 (i.e., this is the
first note of an arpeggio), then we call ArpTimeProc() to initiate an arpeggiation.
(ArpTimeProc() is the routine that actually writes out the MIDI data.) If the status
byte in the packet is a note-off message, however, we locate the matching note in the
note table; and, if it’s still a valid note (it didn’t get “stolen”), then we simply delete
it from the note table and decrement the table’s current note count NumNotes. (A note
can get “stolen” if there are 32 notes in our note table and a new note that is lower
than the highest note is inserted.) Finally, we restore the system’s A5 world and
return.
As mentioned above, we call our timeProc ArpTimeProc() from our readHook
ArpReader() when we get the first note of an arpeggiation series. However, it is quite
common for the MIDI Manager to call a timeProc at interrupt level (per an application
scheduled event that will be illustrated below). This can be set up by calling the MIDI
Manager routine MIDIWakeUp() with several arguments which include the reference
number of a specific time port, a time, a period, and a pointer to the timeProc. In this
case, the MIDI Manager calls the timeProc with the time port’s current time and
refCon.
When our readHook ArpReader() calls our timeProc ArpTimeProc(), however,
it passes it the time stamp of the current MIDI packet (which in this case is not used)
and the refCon parameter that was passed to ArpReader(). (The refCon of each port
points to MIDIArp’s global variables.) Once in control, the timeProc again sets up
MIDIArp’s A5 world via its refCon parameter (because it may be called by the MIDI
Manager as well as from within the readHook). We then lock the ArpGlobals structure
so that ArpReader() won’t disturb it. If at this time there are no notes in our note
table, then we simply cancel any pending wake-ups and return. Otherwise, we bump
the note table index to the next note to be played. However, we must avoid the special
case where the first note of an arpeggio may unintentionally be played twice in a row.
(For example, the arpeggiation pattern is “Up,” two notes are struck “at same time,”
and the first note received is higher than the second note. The first note received gets
index 0, noteTbleIndex is set to 0, and the note is played. When the next note is
received, the previous note is moved to index 1, the new note is inserted into index 0,
and the noteTblIndex is bumped to index 1, which is the note that gets played. This
means that the first note will be perceived to be played twice!) To avoid this sequence
of events, if the note that we played the last time the ArpTimeProc() was called
(LastNote) is the same note as our new one, then we bump the note index once more.
Now we can finally write out a note-on message by calling MIDIWritePacket(). And,
because we know the duration of the note, we we can write out its corresponding
note-off by simply adjusting the time stamp and calling MIDIWritePacket() again with
the same packet. The MIDI Manager will make sure that the packet is actually written
out at the specified time.
The last thing we do in ArpTimeProc() is schedule the time that the MIDI
Manager should call ArpTimeProc() to write out the next note (this is what creates
the arpeggio effect). We first set NextNoteOnTime to itself plus the value of Tempo;
however, we want the ArpTimeProc() to be called (and write out the packet) soon
enough before NextNoteOnTime such that the next call to MIDIWritePacket() will be
before the packet’s time stamp expires. Although we call MIDIWritePacket() with an
accurately time-stamped packet, we call it early to avoid the chance of the note being
received late due to processing time. The basic rule of thumb is to always write early,
making sure that the time stamp (which is some time in the future) is accurate.
Finally, we unlock the ArpGlobals structure, restore the system’s A5 world, and
return.
And that’s it! The readHook continues reading incoming MIDI data, and the
timeProc continues scheduling it to be written out. This continues until the user hits
the Quit button. When this happens, we call ArpClose() to save our current patch
configuration into the applications ‘port’ resource and sign-out from the MIDI